[iOS] iOSのDelegateをしっかりと理解する
iOS の通知を知る
こんにちは、iPhone アプリ開発担当の荒川です。
この記事では iOS アプリでの「通知」について紹介します。
今回扱う「通知」とは、「プッシュ通知(Push Notification)」や「ローカル通知(Local Notification)」のことではなく、特定のインスタンスから別のインスタンスへ処理を委譲することを指します。
「委譲(いじょう)」と言われると何やら難しいことのように思えますが、あるクラスに書かれた何かの処理を他のクラスで処理する事だと思って下さい。
iOS アプリケーションでは以下の通知方法がよく使われます。
- Delegate(処理の委譲)
- Key Value Observe(キー値監視)
- Notification Center(情報のブロードキャスト)
この中の Delegate について、Objective-C での実装方法を交えて紹介します。
Delegate の特徴
ある特定のインスタンスを保持する、別のインスタンスへ通知を送ることができます。
通知先は任意のタイミングで通知を受け取ることができ、通知の解除を意識する必要はありません。このため、UIView などのライフサイクルが短いインスタンスにも使いやすいです。
通知したい情報はデリゲートメソッドの引数で自由に定義することができます。
一般的な Framework で Delegate を使っているクラスとしては, UITableView をはじめとした各 UIKit のクラスや、位置情報などを管理するクラス CLLocationManager などがあります。
Delegate を理解するために必要なキーワード
プロトコル
.h ファイルのプロトコル句( @protocol ~ @end )内に処理を委譲したいメソッドを定義します。この句内に定義されたメソッドを デリゲートメソッド と呼びます。
デリゲートメソッド
デリゲートメソッドには、必ず実装すべき @required と、実装しなくてもよい @optional 修飾子があります。
@required 修飾子以降に記述されたデリゲートメソッドを対象クラスで実装しなければ、コンパイルエラーになります。
例として、UITableViewDatasourceプロトコル を実装すべき ViewController があるとします。@required 修飾子が付与されている tableView:numberOfRowsInSection: と tableView:cellForRowAtIndexPath: メソッドを実装しないと、そのViewControllerはコンパイルエラーになります。
@required, @optional 修飾子をどちらも指定しない場合は、デフォルトで @optional 修飾子と同じ挙動になります。
デリゲートインスタンス
プロトコルを定義した .h ファイルには デリゲートインスタンス を定義します。
デリゲートインスタンスは weak参照の id 型 プロパティとして保持されます。
デリゲートパターン
プロトコルを定義してからデリゲートメソッドを実装するまでには以下の手順が必要です。
通知元
- プロトコルを定義
- デリゲートメソッドを定義
- デリゲートインスタンスを定義
- デリゲートメソッドの呼び出しを実装
通知先
- プロトコルに準拠
- デリゲートインスタンスに自身をセット
- デリゲートメソッドの処理を実装
実際に順を追ってコードを記述してみましょう。
ViewController クラスから SampleViewController にモーダル遷移し、SampleViewController を閉じる時に ViewController で処理を行いたいと仮定します。
以下はその実装例です。
通知元(SampleViewController)
1, プロトコルを定義
#import <UIKit/UIKit.h>; /** * SampleViewControllerDelegate プロトコル */ @protocol SampleViewDelegate <NSObject> @end /** * モーダル表示される画面クラスです。 * 通知元のクラスになります。 */ @interface SampleViewController : UIViewController @end
2, デリゲートメソッドを定義
#import <UIKit/UIKit.h> // @interface 句より前にプロトコルを定義したい場合、かつ引数に自身のインスタンスを持つ場合には // 自身のクラスを @class 句で定義する必要があります。 @class SampleViewController; /** * SampleViewControllerDelegate プロトコル */ @protocol SampleViewDelegate <NSObject> @optional /** * モーダル表示を閉じる時に呼び出されます。 * 引数なしのデリゲートメソッドです。 */ - (void)dismissSampleView; /** * モーダル表示を閉じる時に呼び出されます。 * 引数を持つデリゲートメソッドです。 * * @param sampleView SampleView インスタンス * @param info SampleView から通知したい情報 */ - (void)sampleView:(SampleViewController *)sampleView willDismissWithInfo:(NSDictionary *)info; @end /** * モーダル表示される画面クラスです。 * 通知元のクラスになります。 */ @interface SampleViewController : UIViewController @end
3, デリゲートインスタンスを定義
#import <UIKit/UIKit.h> // @interface 句より前にプロトコルを定義したい場合、かつ引数に自身のインスタンスを持つ場合には // 自身のクラスを @class 句で定義する必要があります。 @class SampleViewController; /** * SampleViewControllerDelegate プロトコル */ @protocol SampleViewDelegate <NSObject> @optional /** * モーダル表示を閉じる時に呼び出されます。 * 引数なしのデリゲートメソッドです。 */ - (void)dismissSampleView; /** * モーダル表示を閉じる時に呼び出されます。 * 引数を持つデリゲートメソッドです。 * * @param sampleView SampleView インスタンス * @param info SampleView から通知したい情報 */ - (void)sampleView:(SampleViewController *)sampleView willDismissWithInfo:(NSDictionary *)info; @end /** * モーダル表示される画面クラスです。 * 通知元のクラスになります。 */ @interface SampleViewController : UIViewController /** * SampleViewDelegate インスタンス * 通知先のインスタンスが入ります。 */ @property (nonatomic, weak) id<SampleViewDelegate> delegate; @end
上記で閉じた時に呼び出されるメソッド dismissSampleView と、閉じる時に何か情報を渡してくれるメソッド sampleView: willDismissWithInfo を定義しました。
デリゲートメソッドの命名として、引数が2つ以上ある場合は、第1引数に自身のインスタンス(今回はSampleViewControllerインスタンス)を渡す慣習があります。
定義は以上です。次に、デリゲートメソッドの呼び出しを実装します。
4, デリゲートメソッドの呼び出しを実装
#import "SampleViewController.h" @interface SampleViewController () @property (nonatomic, strong) NSDictionary *anyInfo; @end @implementation SampleViewController #pragma mark - Lifecycle methods - (void)viewDidLoad { [super viewDidLoad]; self.anyInfo = @{@"date" : [NSDate date]}; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } #pragma mark - IBAction method // 閉じるボタンを押す時に呼び出されます。 - (IBAction)didTapCloseButton:(id)sender { // デリゲートインスタンスが dismissSampleView メソッドを実装しているか確認します。 if ([self.delegate respondsToSelector:@selector(dismissSampleView)]) { // dismissSampleView が実装されているので、処理をデリゲートインスタンスに委譲します。 [self.delegate dismissSampleView]; } // デリゲートインスタンスが sampleView:willDismissWithInfo: メソッドを実装しているか確認します。 if ([self.delegate respondsToSelector:@selector(sampleView:willDismissWithInfo:)]) { // sampleView:willDismissWithInfo: が実装されているので、処理をデリゲートインスタンスに委譲します。 [self.delegate sampleView:self willDismissWithInfo:self.anyInfo]; } [self dismissViewControllerAnimated:YES completion:nil]; } @end
具体的な処理の内容は通知先に記述します。通知元の実装は以上です。
通知先(ViewController)
1, プロトコルに準拠
#import "ViewController.h" #import "SampleViewController.h" @interface ViewController () <SampleViewDelegate> @end
2, デリゲートインスタンスに自身をセット
@implementation ViewController ... 省略 #pragma mark - Segue method // モーダル遷移を行う時に呼び出されます。 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"SampleView"]) { SampleViewController *sampleViewController = segue.destinationViewController; // SampleViewController がインスタンス化されたタイミングでデリゲートインスタンス(self)をセット sampleViewController.delegate = self; } } @end
3, デリゲートメソッドの処理を実装
@implementation ViewController ... 省略 #pragma mark - SampleViewDelegate methods - (void)dismissSampleView { NSLog(@"SampleViewが閉じられるタイミングで処理が行えます。"); } - (void)sampleView:(SampleViewController *)sampleView willDismissWithInfo:(NSDictionary *)info { NSLog(@"SampleViewが閉じられるタイミングで、info %@ を受け取って処理が行えます。", info); } @end
以上でデリゲートパターンの実装は完了です。
サンプルコードのプロジェクトを GitHub にアップロードしましたので、確認したい方は参考にして下さい。
まとめ
iOS の Delegate の実装例などはググるといっぱい出ます。しかし、最近の記事は少なく、かつ手順までしっかりと理解しやすいのはあんまりないなと思ったのでまとめようと思いました。
また、Delegate の実装方法をしっかりと理解していないと自分で書けない(しかしコピペで書ける)事になりそうなので、該当した方はこの際にしっかりと覚えられると良いでしょう。
何番煎じだかわかりませんが参考になれば筆者が喜びます。